/*
 * A smart gesture password locker for react-native apps
 * https://github.com/react-native-component/react-native-smart-sudoku-grid/
 * Released under the MIT license
 * Copyright (c) 2016 react-native-component <moonsunfall@aliyun.com>
 */

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { PanResponder, Dimensions, StyleSheet, View } from 'react-native'

import * as Utils from '../utils/utils'
import Point from './Point'
import Line from './Line'
import Arrow from './Arrow'

const styles = StyleSheet.create({
  container: {
    flex: 1,
    overflow: 'hidden',
  },
})

const padding = 8
const { width: deviceWidth } = Dimensions.get('window')

export default class GesturePassword extends Component {
  static defaultProps = {
    lineWidth: 10,
    pointBackgroundColor: 'transparent',
    gestureAreaLength: 280,
    color: '#A9A9A9',
    // lineColor: '#A9A9A9',
    activeColor: '#00AAEF',
    warningColor: 'red',
    warningDuration: 0,
    isWarning: false,
    showArrow: true,
    allowCross: true,
  }

  static propTypes = {
    lineWidth: PropTypes.number,
    pointBackgroundColor: PropTypes.string,
    gestureAreaLength: PropTypes.number,
    color: PropTypes.string,
    lineColor: PropTypes.string,
    activeColor: PropTypes.string,
    warningColor: PropTypes.string,
    warningDuration: PropTypes.number,
    topComponent: PropTypes.element,
    bottomComponent: PropTypes.element,
    isWarning: PropTypes.bool,
    showArrow: PropTypes.bool,
    allowCross: PropTypes.bool,
    onStart: PropTypes.func,
    onReset: PropTypes.func,
    onFinish: PropTypes.func,
  }

  // 构造
  constructor(props) {
    super(props)

    // 初始状态
    this.state = {
      isWarning: false,
      points: [],
      lines: [],
      arrows: [],
    }

    this._gestureAreaMarginHorizontal =
      (deviceWidth - props.gestureAreaLength) / 2
    this._gestureAreaLeft = 0
    this._gestureAreaTop = 0
    this._pointRadius = (props.gestureAreaLength - padding * 2) / 8
    this._currentPoint = null
    this._currentLine = null
    this._timer = null
    this._sequence = []
  }

  UNSAFE_componentWillMount() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,
      onPanResponderGrant: this._onTouchStart,
      onPanResponderMove: this._onTouchMove,
      onPanResponderRelease: this._onTouchEnd,
      onPanResponderTerminationRequest: () => false,
    })
  }

  UNSAFE_componentWillReceiveProps(nextProps) {
    this.setState({
      isWarning: nextProps.isWarning,
    })
  }

  render() {
    return (
      <View style={[this.props.style, styles.container]}>
        {this.props.topComponent}
        <View
          {...this._panResponder.panHandlers}
          onLayout={this._onLayout}
          style={{
            overflow: 'hidden',
            width: this.props.gestureAreaLength,
            height: this.props.gestureAreaLength,
            marginHorizontal: this._gestureAreaMarginHorizontal,
          }}
        >
          {this._renderLines()}
          {this._renderPoints()}
          {this.props.showArrow ? this._renderArrows() : null}
        </View>
        {this.props.bottomComponent}
      </View>
    )
  }

  componentWillUnmount() {
    if (this._timer != null) {
      clearTimeout(this._timer)
      this._timer = null
    }
  }

  _onLayout = (e) => {
    this._gestureAreaLeft = e.nativeEvent.layout.x
    this._gestureAreaTop = e.nativeEvent.layout.y + 66
    this._initializePoints()
  }

  _renderArrows() {
    return this.state.arrows.map((arrow, index) => {
      if (this.state.isWarning) {
        arrow.color = this.props.warningColor
      }
      return (
        <Arrow
          key={'arrow-' + index}
          width={this._pointRadius / 3}
          color={arrow.color}
          start={{
            x: arrow.start.x - this._gestureAreaLeft,
            y: arrow.start.y - this._gestureAreaTop,
          }}
          end={{
            x: arrow.end.x - this._gestureAreaLeft,
            y: arrow.end.y - this._gestureAreaTop,
          }}
        />
      )
    })
  }

  _renderPoints() {
    return this.state.points.map((point, index) => {
      return (
        <Point
          key={'point-' + index}
          radius={this._pointRadius}
          borderWidth={0}
          backgroundColor={this.props.pointBackgroundColor}
          color={this.props.color}
          activeColor={this.props.activeColor}
          warningColor={this.props.warningColor}
          isActive={point.isActive}
          isWarning={point.isActive ? this.state.isWarning : false}
          index={point.index}
          position={point.position}
        />
      )
    })
  }

  _renderLines() {
    return this.state.lines.map((line, index) => {
      if (this.state.isWarning) {
        line.color = this.props.warningColor
      }
      return (
        <Line
          key={'line-' + index}
          color={line.color}
          lineWidth={this.props.lineWidth}
          start={{
            x: line.start.x - this._gestureAreaLeft,
            y: line.start.y - this._gestureAreaTop,
          }}
          end={{
            x: line.end.x - this._gestureAreaLeft,
            y: line.end.y - this._gestureAreaTop,
          }}
        />
      )
    })
  }

  _initializePoints() {
    // avoid repeat invoking(for android)
    if (this.state.points.length) {
      return
    }

    const points = []
    for (let i = 0; i < 9; i++) {
      const left = this._pointRadius * 3 * (i % 3) + padding
      const top = this._pointRadius * 3 * Math.floor(i / 3) + padding
      points.push({
        index: i,
        position: {
          left: left,
          top: top,
        },
        origin: {
          x: this._gestureAreaLeft + left + this._pointRadius,
          y: this._gestureAreaTop + top + this._pointRadius,
        },
        isActive: true,
        isWarning: false,
      })
    }
    this.setState({
      points,
    })
  }

  _getTouchPoint(location) {
    for (const point of this.state.points) {
      if (Utils.isPointInPath(location, point.origin, this._pointRadius)) {
        return point
      }
    }
    return null
  }

  _addSequence(index) {
    if (this._sequence.includes(index)) {
      return
    }
    this._sequence.push(index)
  }

  _addArrow(arrow) {
    this.state.arrows.push(arrow)
    const arrows = this.state.arrows
    this.setState({ arrows })
  }

  _addLine(line) {
    this.state.lines.push(line)
    const lines = this.state.lines
    this.setState({ lines })
  }

  _updateLine(start, end) {
    this._currentLine.start = start
    this._currentLine.end = end

    const lines = this.state.lines
    this.setState({ lines })
  }

  _setToActive(point) {
    point.isActive = false
    this.setState({ points: this.state.points })
  }

  _reset() {
    const points = this.state.points.map((point, index) => {
      point.isActive = true
      return point
    })
    this.setState({
      isWarning: false,
      points: points,
      lines: [],
      arrows: [],
    })

    this._sequence = []
    this._currentPoint = null
    this._currentLine = null

    if (this.props.onReset) {
      this.props.onReset()
    }
  }

  _onTouchStart = (e, gestureState) => {
    if (this.props.onStart) {
      this.props.onStart()
    }

    if (this._timer != null) {
      clearTimeout(this._timer)
      this._timer = null
    }

    this._reset()
    const location = {
      x: e.nativeEvent.pageX,
      y: e.nativeEvent.pageY,
    }
    const point = this._getTouchPoint(location)
    if (point == null) {
      return
    }

    this._addSequence(point.index)
    this._setToActive(point)
    this._currentPoint = point
  }

  _onTouchMove = (e, gestureState) => {
    const location = {
      x: e.nativeEvent.pageX,
      y: e.nativeEvent.pageY,
    }
    const point = this._getTouchPoint(location)

    if (point == null) {
      if (this._currentLine == null) {
        return
      }
      this._updateLine(this._currentPoint.origin, location)
    } else {
      if (this._currentLine == null) {
        const line = {
          start: point.origin,
          end: location,
          color: this.props.lineColor || this.props.activeColor,
        }
        this._addLine(line)
        this._currentLine = line

        if (this._currentPoint != null) {
          return
        }
        this._addSequence(point.index)
        this._setToActive(point)
        this._currentPoint = point
      } else {
        if (point === this._currentPoint) {
          this._updateLine(point.origin, location)
          return
        }

        // if (~this._sequence.findIndex((item) => item === point.index)) {
        if (this._sequence.includes(point.index)) {
          this._updateLine(this._currentPoint.origin, location)
          return
        }

        if (!this.props.allowCross) {
          const crossPoint = Utils.getCrossPoint(
            this.state.points,
            this._currentPoint,
            point,
            this._pointRadius
          )
          if (crossPoint != null) {
            this._addSequence(crossPoint.index)
            this._setToActive(crossPoint)
          }
        }

        this._updateLine(this._currentPoint.origin, point.origin)
        const arrow = {
          start: this._currentPoint.origin,
          end: point.origin,
          color: this.props.activeColor,
        }
        this._addArrow(arrow)
        const line = {
          start: point.origin,
          end: location,
          color: this.props.lineColor || this.props.activeColor,
        }
        this._addLine(line)
        this._currentLine = line

        this._addSequence(point.index)
        this._setToActive(point)
        this._currentPoint = point
      }
    }
  }

  _onTouchEnd = (e, gestureState) => {
    if (this._sequence.length === 0) {
      return
    }

    const points = this.state.points
    const lines = this.state.lines
    lines.pop()

    this.setState({
      lines,
      points,
    })

    const password = Utils.getPassword(this._sequence)
    if (this.props.onFinish) {
      this.props.onFinish(password)
    }

    if (this.props.warningDuration > 0) {
      this._timer = setTimeout(() => {
        this._reset()
      }, this.props.warningDuration)
    } else {
      this._reset()
    }
  }
}
